#include <doctest.h>

#include "WPTexImageParser.hpp"
#include "Fs/VFS.h"
#include "Fs/MemBinaryStream.h"
#include "Type.hpp"

#include <cstring>
#include <lz4.h>
#include <memory>
#include <unordered_map>
#include <vector>

using namespace wallpaper;
using namespace wallpaper::fs;

// ---------------------------------------------------------------------------
// Mock filesystem: serves pre-built binary blobs keyed by VFS-internal path
// ---------------------------------------------------------------------------
class MockFs : public Fs {
public:
    void AddFile(std::string path, std::vector<uint8_t> data) {
        m_files[std::move(path)] = std::move(data);
    }

    bool Contains(std::string_view path) const override {
        return m_files.count(std::string(path)) > 0;
    }

    std::shared_ptr<IBinaryStream> Open(std::string_view path) override {
        m_openCount[std::string(path)]++;
        auto it = m_files.find(std::string(path));
        if (it == m_files.end()) return nullptr;
        auto copy = it->second; // copy so stream owns its data
        return std::make_shared<MemBinaryStream>(std::move(copy));
    }

    std::shared_ptr<IBinaryStreamW> OpenW(std::string_view) override { return nullptr; }

    // How many times was a given VFS-internal path opened?
    int openCount(const std::string& path) const {
        auto it = m_openCount.find(path);
        return it == m_openCount.end() ? 0 : it->second;
    }

private:
    std::unordered_map<std::string, std::vector<uint8_t>> m_files;
    std::unordered_map<std::string, int>                  m_openCount;
};

// ---------------------------------------------------------------------------
// Binary builder helpers
// ---------------------------------------------------------------------------
namespace
{

// Append raw bytes
void append(std::vector<uint8_t>& buf, const void* data, size_t n) {
    auto p = static_cast<const uint8_t*>(data);
    buf.insert(buf.end(), p, p + n);
}

void appendInt32(std::vector<uint8_t>& buf, int32_t v) { append(buf, &v, 4); }
void appendUint32(std::vector<uint8_t>& buf, uint32_t v) { append(buf, &v, 4); }
void appendFloat(std::vector<uint8_t>& buf, float v) { append(buf, &v, 4); }

// Write a 9-byte TEX version header (matches WriteVersion format)
void appendTexVersion(std::vector<uint8_t>& buf, int ver) {
    char hdr[9] {};
    std::snprintf(hdr, sizeof(hdr), "%.4s%.4d", "TEX", ver);
    append(buf, hdr, 9);
}

// Build the common .tex header portion (TEXV+TEXI+fields+TEXB)
// Returns the buffer so callers can append image data / sprite data after it
std::vector<uint8_t> makeTexHeader(int texvVer, int texiVer, int texbVer, int32_t type,
                                   uint32_t flags, int32_t width, int32_t height, int32_t mapWidth,
                                   int32_t mapHeight, int32_t count) {
    std::vector<uint8_t> buf;
    buf.reserve(256);
    appendTexVersion(buf, texvVer); // TEXV
    appendTexVersion(buf, texiVer); // TEXI
    appendInt32(buf, type);         // texture type
    appendUint32(buf, flags);       // flags
    appendInt32(buf, width);
    appendInt32(buf, height);
    appendInt32(buf, mapWidth);
    appendInt32(buf, mapHeight);
    appendInt32(buf, 0);            // unknown, skipped
    appendTexVersion(buf, texbVer); // TEXB
    appendInt32(buf, count);
    return buf;
}

// Append one mipmap's data for texb==1 (no LZ4 fields)
void appendMipmapV1(std::vector<uint8_t>& buf, int32_t w, int32_t h,
                    const std::vector<uint8_t>& pixels) {
    appendInt32(buf, w);
    appendInt32(buf, h);
    appendInt32(buf, static_cast<int32_t>(pixels.size()));
    append(buf, pixels.data(), pixels.size());
}

// Append one mipmap's data for texb==2 (with LZ4 fields)
void appendMipmapV2(std::vector<uint8_t>& buf, int32_t w, int32_t h,
                    const std::vector<uint8_t>& pixels, bool compress = false) {
    appendInt32(buf, w);
    appendInt32(buf, h);
    if (compress) {
        int               maxDst = LZ4_compressBound(static_cast<int>(pixels.size()));
        std::vector<char> compressed(static_cast<size_t>(maxDst));
        int compressedSize = LZ4_compress_default(reinterpret_cast<const char*>(pixels.data()),
                                                  compressed.data(),
                                                  static_cast<int>(pixels.size()),
                                                  maxDst);
        appendInt32(buf, 1); // LZ4_compressed = true
        appendInt32(buf, static_cast<int32_t>(pixels.size()));
        appendInt32(buf, compressedSize);
        append(buf, compressed.data(), static_cast<size_t>(compressedSize));
    } else {
        appendInt32(buf, 0); // LZ4_compressed = false
        appendInt32(buf, 0); // decompressed_size (unused when not compressed)
        appendInt32(buf, static_cast<int32_t>(pixels.size()));
        append(buf, pixels.data(), pixels.size());
    }
}

// Build a complete simple RGBA8 .tex file (1 image, 1 mipmap, texb=1, no sprite)
std::vector<uint8_t> makeSimpleRGBA8Tex(int32_t w, int32_t h) {
    auto buf = makeTexHeader(1, 1, 1, /*type=RGBA8*/ 0, /*flags=*/0, w, h, w, h, /*count=*/1);
    // image 0: 1 mipmap
    appendInt32(buf, 1); // mipmap_count
    std::vector<uint8_t> pixels(static_cast<size_t>(w * h * 4), 0xAB);
    appendMipmapV1(buf, w, h, pixels);
    return buf;
}

// Mount a .tex blob into VFS at the path WPTexImageParser expects
void mountTex(VFS& vfs, const std::string& name, std::vector<uint8_t> data) {
    // WPTexImageParser opens "/assets/materials/<name>.tex"
    // VFS mount at "/assets" strips to "/materials/<name>.tex"
    auto mockFs = std::make_unique<MockFs>();
    mockFs->AddFile("/materials/" + name + ".tex", std::move(data));
    vfs.Mount("/assets", std::move(mockFs));
}

} // namespace

// ===========================================================================
// Tests
// ===========================================================================

TEST_SUITE("WPTexImageParser") {
    TEST_CASE("Valid RGBA8 texture — Parse") {
        VFS  vfs;
        auto texData = makeSimpleRGBA8Tex(4, 4);
        mountTex(vfs, "test_rgba8", std::move(texData));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_rgba8");

        REQUIRE(img != nullptr);
        CHECK(img->key == "test_rgba8");
        CHECK(img->header.format == TextureFormat::RGBA8);
        CHECK(img->header.width == 4);
        CHECK(img->header.height == 4);
        CHECK(img->header.count == 1);
        CHECK(img->header.isSprite == false);
        REQUIRE(img->slots.size() == 1);
        REQUIRE(img->slots[0].mipmaps.size() == 1);
        CHECK(img->slots[0].mipmaps[0].width == 4);
        CHECK(img->slots[0].mipmaps[0].height == 4);
        CHECK(img->slots[0].mipmaps[0].size == 4 * 4 * 4);
        // Verify pixel data
        CHECK(img->slots[0].mipmaps[0].data.get()[0] == 0xAB);
    }

    TEST_CASE("Valid RGBA8 texture — ParseHeader") {
        VFS  vfs;
        auto texData = makeSimpleRGBA8Tex(8, 8);
        mountTex(vfs, "test_hdr", std::move(texData));

        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("test_hdr");

        CHECK(header.format == TextureFormat::RGBA8);
        CHECK(header.width == 8);
        CHECK(header.height == 8);
        CHECK(header.count == 1);
        CHECK(header.isSprite == false);
    }

    TEST_CASE("BC1 (DXT1) format") {
        // type=7 → BC1
        auto buf = makeTexHeader(1, 1, 1, 7, 0, 4, 4, 4, 4, 1);
        // BC1: 8 bytes per 4x4 block = 8 bytes for a 4x4 texture
        appendInt32(buf, 1); // mipmap_count
        std::vector<uint8_t> blockData(8, 0);
        appendMipmapV1(buf, 4, 4, blockData);

        VFS vfs;
        mountTex(vfs, "test_bc1", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_bc1");
        REQUIRE(img != nullptr);
        CHECK(img->header.format == TextureFormat::BC1);
    }

    TEST_CASE("BC3 (DXT5) format") {
        auto buf = makeTexHeader(1, 1, 1, 4, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, 1);
        std::vector<uint8_t> blockData(16, 0); // 16 bytes per 4x4 block
        appendMipmapV1(buf, 4, 4, blockData);

        VFS vfs;
        mountTex(vfs, "test_bc3", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_bc3");
        REQUIRE(img != nullptr);
        CHECK(img->header.format == TextureFormat::BC3);
    }

    TEST_CASE("BC2 (DXT3) format") {
        auto buf = makeTexHeader(1, 1, 1, 6, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, 1);
        std::vector<uint8_t> blockData(16, 0);
        appendMipmapV1(buf, 4, 4, blockData);

        VFS vfs;
        mountTex(vfs, "test_bc2", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_bc2");
        REQUIRE(img != nullptr);
        CHECK(img->header.format == TextureFormat::BC2);
    }

    TEST_CASE("RG8 format") {
        auto buf = makeTexHeader(1, 1, 1, 8, 0, 2, 2, 2, 2, 1);
        appendInt32(buf, 1);
        std::vector<uint8_t> pixels(2 * 2 * 2, 0);
        appendMipmapV1(buf, 2, 2, pixels);

        VFS vfs;
        mountTex(vfs, "test_rg8", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_rg8");
        REQUIRE(img != nullptr);
        CHECK(img->header.format == TextureFormat::RG8);
    }

    TEST_CASE("R8 format") {
        auto buf = makeTexHeader(1, 1, 1, 9, 0, 2, 2, 2, 2, 1);
        appendInt32(buf, 1);
        std::vector<uint8_t> pixels(2 * 2, 0);
        appendMipmapV1(buf, 2, 2, pixels);

        VFS vfs;
        mountTex(vfs, "test_r8", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_r8");
        REQUIRE(img != nullptr);
        CHECK(img->header.format == TextureFormat::R8);
    }

    TEST_CASE("Unknown format type falls back to RGBA8") {
        auto buf = makeTexHeader(1, 1, 1, 99, 0, 2, 2, 2, 2, 1);
        appendInt32(buf, 1);
        std::vector<uint8_t> pixels(2 * 2 * 4, 0);
        appendMipmapV1(buf, 2, 2, pixels);

        VFS vfs;
        mountTex(vfs, "test_unk", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_unk");
        REQUIRE(img != nullptr);
        CHECK(img->header.format == TextureFormat::RGBA8);
    }

    TEST_CASE("Alias texture fallback — missing file") {
        VFS vfs;
        // Mount an empty filesystem so Contains returns false
        auto mockFs = std::make_unique<MockFs>();
        vfs.Mount("/assets", std::move(mockFs));

        WPTexImageParser parser(&vfs);

        SUBCASE("Parse returns 1x1 white fallback") {
            auto img = parser.Parse("_alias_missing");
            REQUIRE(img != nullptr);
            CHECK(img->key == "_alias_missing");
            CHECK(img->header.width == 1);
            CHECK(img->header.height == 1);
            CHECK(img->header.format == TextureFormat::RGBA8);
            REQUIRE(img->slots.size() == 1);
            REQUIRE(img->slots[0].mipmaps.size() == 1);
            auto* px = img->slots[0].mipmaps[0].data.get();
            CHECK(px[0] == 255);
            CHECK(px[1] == 255);
            CHECK(px[2] == 255);
            CHECK(px[3] == 255);
        }

        SUBCASE("ParseHeader returns 1x1 fallback header") {
            auto header = parser.ParseHeader("_alias_missing");
            CHECK(header.width == 1);
            CHECK(header.height == 1);
            CHECK(header.format == TextureFormat::RGBA8);
            CHECK(header.count == 1);
        }
    }

    TEST_CASE("Alias texture with existing file — parses normally") {
        VFS  vfs;
        auto texData = makeSimpleRGBA8Tex(2, 2);
        mountTex(vfs, "_alias_exists", std::move(texData));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("_alias_exists");
        REQUIRE(img != nullptr);
        CHECK(img->header.width == 2);
        CHECK(img->header.height == 2);
    }

    TEST_CASE("Sprite with out-of-range imageId — disables sprite") {
        // Build a sprite texture: 1 image, sprite flag set
        uint32_t spriteFlag = (1u << 2); // WPTexFlagEnum::sprite = bit 2
        auto     buf        = makeTexHeader(1, 1, 2, 0, spriteFlag, 16, 16, 16, 16, 1);

        // Image 0: 1 mipmap (texb=2, so LZ4 fields present)
        appendInt32(buf, 1); // mipmap_count
        std::vector<uint8_t> pixels(16 * 16 * 4, 0);
        appendMipmapV2(buf, 16, 16, pixels);

        // Sprite section
        appendTexVersion(buf, 2); // texs version
        appendInt32(buf, 1);      // framecount = 1

        // Frame with out-of-range imageId (only 1 image, so imageId=1 is OOB)
        appendInt32(buf, 1);     // imageId = 1 (invalid, only index 0 exists)
        appendFloat(buf, 0.1f);  // frametime
        appendFloat(buf, 0.0f);  // x
        appendFloat(buf, 0.0f);  // y
        appendFloat(buf, 16.0f); // xAxis[0]
        appendFloat(buf, 0.0f);  // xAxis[1]
        appendFloat(buf, 0.0f);  // yAxis[0]
        appendFloat(buf, 16.0f); // yAxis[1]

        VFS vfs;
        mountTex(vfs, "sprite_oob", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_oob");
        // Should disable sprite instead of crashing
        CHECK(header.isSprite == false);
    }

    TEST_CASE("Sprite with negative imageId — disables sprite") {
        uint32_t spriteFlag = (1u << 2);
        auto     buf        = makeTexHeader(1, 1, 2, 0, spriteFlag, 8, 8, 8, 8, 1);

        appendInt32(buf, 1); // mipmap_count
        std::vector<uint8_t> pixels(8 * 8 * 4, 0);
        appendMipmapV2(buf, 8, 8, pixels);

        appendTexVersion(buf, 2); // texs
        appendInt32(buf, 1);      // framecount

        appendInt32(buf, -1); // imageId = -1 (invalid)
        appendFloat(buf, 0.1f);
        appendFloat(buf, 0.0f);
        appendFloat(buf, 0.0f);
        appendFloat(buf, 8.0f);
        appendFloat(buf, 0.0f);
        appendFloat(buf, 0.0f);
        appendFloat(buf, 8.0f);

        VFS vfs;
        mountTex(vfs, "sprite_neg", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_neg");
        CHECK(header.isSprite == false);
    }

    TEST_CASE("Valid sprite — parsed correctly") {
        uint32_t spriteFlag = (1u << 2);
        auto     buf        = makeTexHeader(1, 1, 2, 0, spriteFlag, 16, 16, 16, 16, 1);

        appendInt32(buf, 1);
        std::vector<uint8_t> pixels(16 * 16 * 4, 0);
        appendMipmapV2(buf, 16, 16, pixels);

        appendTexVersion(buf, 2); // texs
        appendInt32(buf, 1);      // framecount = 1

        appendInt32(buf, 0);     // imageId = 0 (valid)
        appendFloat(buf, 0.5f);  // frametime
        appendFloat(buf, 0.0f);  // x
        appendFloat(buf, 0.0f);  // y
        appendFloat(buf, 16.0f); // xAxis[0]
        appendFloat(buf, 0.0f);  // xAxis[1]
        appendFloat(buf, 0.0f);  // yAxis[0]
        appendFloat(buf, 16.0f); // yAxis[1]

        VFS vfs;
        mountTex(vfs, "sprite_ok", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_ok");
        CHECK(header.isSprite == true);
        CHECK(header.spriteAnim.numFrames() == 1);
        auto& frame = header.spriteAnim.GetCurFrame();
        CHECK(frame.imageId == 0);
        CHECK(frame.frametime == doctest::Approx(0.5f));
    }

    TEST_CASE("Sprite mipmap_pow2 with non-square image (kills w*h→w/h mutant)") {
        // 2x4 sprite: width*height = 8 (isPow2=true), width/height = 0 (isPow2=false)
        uint32_t spriteFlag = (1u << 2);
        auto     buf        = makeTexHeader(1, 1, 2, 0, spriteFlag, 2, 4, 2, 4, 1);

        // Image 0: 1 mipmap, 2x4
        appendInt32(buf, 1);                       // mipmap_count
        std::vector<uint8_t> pixels(2 * 4 * 4, 0); // 2x4 RGBA
        appendMipmapV2(buf, 2, 4, pixels);

        // Sprite section
        appendTexVersion(buf, 2); // texs version
        appendInt32(buf, 1);      // framecount = 1

        appendInt32(buf, 0);    // imageId = 0
        appendFloat(buf, 0.1f); // frametime
        appendFloat(buf, 0.0f); // x
        appendFloat(buf, 0.0f); // y
        appendFloat(buf, 2.0f); // xAxis[0]
        appendFloat(buf, 0.0f); // xAxis[1]
        appendFloat(buf, 0.0f); // yAxis[0]
        appendFloat(buf, 4.0f); // yAxis[1]

        VFS vfs;
        mountTex(vfs, "sprite_2x4", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_2x4");
        CHECK(header.isSprite == true);
        // 2 * 4 = 8 → isPow2 = true
        // If mutated to 2 / 4 = 0 → isPow2 = false (KILLED)
        CHECK(header.mipmap_pow2 == true);
    }

    TEST_CASE("Sprite mipmap_larger true when atlas exceeds map (padded picture)") {
        // A plain picture stored as a single sprite frame inside a larger
        // power-of-two container: 10x8 content in a 16x16 .tex.  ParseHeader's
        // sprite branch used to leave mipmap_larger at its default false (only
        // the full Parse() path computed it), so WPSceneParser collapsed the
        // image's UV mapRate to {1,1} and the picture filled just the map/tex
        // fraction of its quad.  Driver: Tomb Raider 1179142239 background
        // (3500x2188 image in a 4096x4096 .tex rendered into the top-left).
        uint32_t spriteFlag = (1u << 2);
        auto     buf        = makeTexHeader(1, 1, 2, 0, spriteFlag, 16, 16, 10, 8, 1);

        appendInt32(buf, 1); // mipmap_count
        std::vector<uint8_t> pixels(16 * 16 * 4, 0);
        appendMipmapV2(buf, 16, 16, pixels);

        appendTexVersion(buf, 2); // texs
        appendInt32(buf, 1);      // framecount = 1
        appendInt32(buf, 0);      // imageId = 0
        appendFloat(buf, 0.1f);   // frametime
        appendFloat(buf, 0.0f);   // x
        appendFloat(buf, 0.0f);   // y
        appendFloat(buf, 10.0f);  // xAxis[0] (content width)
        appendFloat(buf, 0.0f);   // xAxis[1]
        appendFloat(buf, 0.0f);   // yAxis[0]
        appendFloat(buf, 8.0f);   // yAxis[1] (content height)

        VFS vfs;
        mountTex(vfs, "sprite_padded", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_padded");
        CHECK(header.isSprite == true);
        CHECK(header.mapWidth == 10);
        CHECK(header.mapHeight == 8);
        // mip0 area 16*16=256 > map area 10*8=80 → must be true.
        CHECK(header.mipmap_larger == true);
    }

    TEST_CASE("Sprite mipmap_larger false when atlas equals map (animated sheet)") {
        // Real animated sprite sheets store map == atlas (per-frame size lives
        // in the frame axes, not map dims), so the padded-picture fix must NOT
        // flip mipmap_larger for them — otherwise their g_Texture0Resolution
        // would change and shift particle UVs.
        uint32_t spriteFlag = (1u << 2);
        auto     buf        = makeTexHeader(1, 1, 2, 0, spriteFlag, 16, 16, 16, 16, 1);

        appendInt32(buf, 1); // mipmap_count
        std::vector<uint8_t> pixels(16 * 16 * 4, 0);
        appendMipmapV2(buf, 16, 16, pixels);

        appendTexVersion(buf, 2); // texs
        appendInt32(buf, 1);      // framecount = 1
        appendInt32(buf, 0);      // imageId = 0
        appendFloat(buf, 0.1f);   // frametime
        appendFloat(buf, 0.0f);   // x
        appendFloat(buf, 0.0f);   // y
        appendFloat(buf, 16.0f);  // xAxis[0]
        appendFloat(buf, 0.0f);   // xAxis[1]
        appendFloat(buf, 0.0f);   // yAxis[0]
        appendFloat(buf, 16.0f);  // yAxis[1]

        VFS vfs;
        mountTex(vfs, "sprite_sheet_eq", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_sheet_eq");
        CHECK(header.isSprite == true);
        // mip0 area 16*16=256 == map area 16*16=256, not greater → false.
        CHECK(header.mipmap_larger == false);
    }

    TEST_CASE("Sprite frame width computed from xAxis magnitude (kills +→- mutant)") {
        // xAxis = (3, 4) → width = sqrt(9+16) = 5
        // If mutated to sqrt(9-16) = sqrt(-7) → NaN → frame.width != 5
        uint32_t spriteFlag = (1u << 2);
        auto     buf        = makeTexHeader(1, 1, 2, 0, spriteFlag, 10, 10, 10, 10, 1);

        appendInt32(buf, 1);
        std::vector<uint8_t> pixels(10 * 10 * 4, 0);
        appendMipmapV2(buf, 10, 10, pixels);

        appendTexVersion(buf, 2);
        appendInt32(buf, 1);

        appendInt32(buf, 0);    // imageId
        appendFloat(buf, 0.1f); // frametime
        appendFloat(buf, 0.0f); // x
        appendFloat(buf, 0.0f); // y
        appendFloat(buf, 3.0f); // xAxis[0]
        appendFloat(buf, 4.0f); // xAxis[1] — non-zero!
        appendFloat(buf, 0.0f); // yAxis[0]
        appendFloat(buf, 8.0f); // yAxis[1]

        VFS vfs;
        mountTex(vfs, "sprite_diag", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_diag");
        CHECK(header.isSprite == true);
        auto& frame = header.spriteAnim.GetCurFrame();
        // width = sqrt(3^2 + 4^2) = sqrt(25) = 5
        CHECK(frame.width == doctest::Approx(5.0f));
        // height = sqrt(0 + 64) = 8
        CHECK(frame.height == doctest::Approx(8.0f));
    }

    TEST_CASE("Sprite texs v3 with extra width/height fields") {
        // texs version 3 has extra width/height int32s after framecount
        uint32_t spriteFlag = (1u << 2);
        auto     buf        = makeTexHeader(1, 1, 2, 0, spriteFlag, 8, 8, 8, 8, 1);

        appendInt32(buf, 1);
        std::vector<uint8_t> pixels(8 * 8 * 4, 0);
        appendMipmapV2(buf, 8, 8, pixels);

        appendTexVersion(buf, 3); // texs version 3
        appendInt32(buf, 1);      // framecount
        appendInt32(buf, 128);    // extra width (texs==3)
        appendInt32(buf, 128);    // extra height (texs==3)

        // Frame data (floats since texs >= 2)
        appendInt32(buf, 0);    // imageId
        appendFloat(buf, 0.2f); // frametime
        appendFloat(buf, 0.0f); // x
        appendFloat(buf, 0.0f); // y
        appendFloat(buf, 8.0f); // xAxis[0]
        appendFloat(buf, 0.0f); // xAxis[1]
        appendFloat(buf, 0.0f); // yAxis[0]
        appendFloat(buf, 8.0f); // yAxis[1]

        VFS vfs;
        mountTex(vfs, "sprite_v3", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_v3");
        CHECK(header.isSprite == true);
        CHECK(header.spriteAnim.numFrames() == 1);
        auto& frame = header.spriteAnim.GetCurFrame();
        CHECK(frame.frametime == doctest::Approx(0.2f));
    }

    TEST_CASE("Sprite texs v1 uses integer frame coordinates") {
        // texs version 1 reads frame x/y/xAxis/yAxis as int32, not float
        uint32_t spriteFlag = (1u << 2);
        auto     buf        = makeTexHeader(1, 1, 2, 0, spriteFlag, 16, 16, 16, 16, 1);

        appendInt32(buf, 1);
        std::vector<uint8_t> pixels(16 * 16 * 4, 0);
        appendMipmapV2(buf, 16, 16, pixels);

        appendTexVersion(buf, 1); // texs version 1
        appendInt32(buf, 1);      // framecount

        // Frame data (int32s for texs==1)
        appendInt32(buf, 0);    // imageId
        appendFloat(buf, 0.3f); // frametime (still float)
        appendInt32(buf, 2);    // x (int)
        appendInt32(buf, 3);    // y (int)
        appendInt32(buf, 6);    // xAxis[0] (int)
        appendInt32(buf, 8);    // xAxis[1] (int)
        appendInt32(buf, 0);    // yAxis[0] (int)
        appendInt32(buf, 10);   // yAxis[1] (int)

        VFS vfs;
        mountTex(vfs, "sprite_v1", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_v1");
        CHECK(header.isSprite == true);
        auto& frame = header.spriteAnim.GetCurFrame();
        CHECK(frame.frametime == doctest::Approx(0.3f));
        // x = 2/16 = 0.125, y = 3/16 = 0.1875
        CHECK(frame.x == doctest::Approx(2.0f / 16.0f));
        CHECK(frame.y == doctest::Approx(3.0f / 16.0f));
        // width = sqrt(6^2 + 8^2) = sqrt(100) = 10
        CHECK(frame.width == doctest::Approx(10.0f));
    }

    TEST_CASE("Sprite mipmap_pow2 with non-power-of-two product") {
        // 3x5 sprite: width*height = 15 (isPow2=false), width/height = 0 (isPow2=false)
        // This test verifies the multiplication is correct for non-pow2 dimensions
        uint32_t spriteFlag = (1u << 2);
        auto     buf        = makeTexHeader(1, 1, 2, 0, spriteFlag, 3, 5, 3, 5, 1);

        appendInt32(buf, 1);
        std::vector<uint8_t> pixels(3 * 5 * 4, 0);
        appendMipmapV2(buf, 3, 5, pixels);

        appendTexVersion(buf, 2);
        appendInt32(buf, 1);

        appendInt32(buf, 0);
        appendFloat(buf, 0.1f);
        appendFloat(buf, 0.0f);
        appendFloat(buf, 0.0f);
        appendFloat(buf, 3.0f);
        appendFloat(buf, 0.0f);
        appendFloat(buf, 0.0f);
        appendFloat(buf, 5.0f);

        VFS vfs;
        mountTex(vfs, "sprite_3x5", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_3x5");
        CHECK(header.isSprite == true);
        CHECK(header.mipmap_pow2 == false); // 3*5=15 not pow2
    }

    TEST_CASE("LZ4 compressed mipmap") {
        auto buf = makeTexHeader(1, 1, 2, 0, 0, 4, 4, 4, 4, 1);

        // Create pixel data and compress it
        std::vector<uint8_t> pixels(4 * 4 * 4);
        for (size_t i = 0; i < pixels.size(); i++) {
            pixels[i] = static_cast<uint8_t>(i & 0xFF);
        }

        appendInt32(buf, 1); // mipmap_count
        appendMipmapV2(buf, 4, 4, pixels, /*compress=*/true);

        VFS vfs;
        mountTex(vfs, "test_lz4", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_lz4");
        REQUIRE(img != nullptr);
        REQUIRE(img->slots.size() == 1);
        REQUIRE(img->slots[0].mipmaps.size() == 1);
        CHECK(img->slots[0].mipmaps[0].size == static_cast<isize>(pixels.size()));
        // Verify decompressed data matches original
        auto* data = img->slots[0].mipmaps[0].data.get();
        for (size_t i = 0; i < pixels.size(); i++) {
            CHECK(data[i] == pixels[i]);
        }
    }

    TEST_CASE("Oversized decompressed_size returns nullptr (OOM guard)") {
        // Fuzz finding (oom-f282830d880e0ebc5ae123ee52c5fe2a20c1d163): a
        // crafted texb=2 mipmap header with a decompressed_size in the
        // hundreds-of-MB range triggers std::vector<char>(N) inside
        // Lz4Decompress before LZ4 even runs.  The 256 MB cap rejects the
        // header instead of attempting the allocation.
        auto buf = makeTexHeader(1, 1, 2, 0, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, 1);                                       // mipmap_count
        appendInt32(buf, 4);                                       // width
        appendInt32(buf, 4);                                       // height
        appendInt32(buf, 1);                                       // LZ4_compressed = true
        appendInt32(buf, 512 * 1024 * 1024);                       // decompressed_size = 512 MB
        appendInt32(buf, 16);                                      // src_size = 16
        for (int i = 0; i < 16; i++) buf.push_back(0);             // 16 bytes of payload

        VFS vfs;
        mountTex(vfs, "test_oom_dsize", std::move(buf));

        WPTexImageParser parser(&vfs);
        CHECK(parser.Parse("test_oom_dsize") == nullptr);
    }

    TEST_CASE("MP4 placeholder buffer caps oversized dimensions (OOM guard)") {
        // Fuzz finding (oom-aa47057a0c3b665efa41381957e7a441c40a2fda): a
        // crafted .tex header pointed at the MP4-fallback code path with
        // mipmap dimensions in the ten-thousands × ten-thousands range,
        // making std::make_unique<uint8_t[]>(w*h*4) request ~676 MB.  The
        // dimension-only guard (Rgba8ByteSize caps at 64K × 64K = 16 GB)
        // wasn't tight enough; cap raw_size against the same 256 MB limit
        // used for decompressed_size.  Use texb=2 + MP4 magic in the
        // payload so the parser takes the MP4 branch.
        auto buf = makeTexHeader(1, 1, 2, 0, 0, 13000, 13000, 13000, 13000, 1);
        appendInt32(buf, 1);                                       // mipmap_count
        appendInt32(buf, 13000);                                   // width (≈676MB at *4)
        appendInt32(buf, 13000);                                   // height
        appendInt32(buf, 0);                                       // LZ4_compressed = false
        appendInt32(buf, 0);                                       // decompressed_size (unused)
        appendInt32(buf, 16);                                      // src_size
        // 16 bytes of payload with "ftyp" at offset 4 to trigger MP4 branch
        for (int i = 0; i < 4; i++) buf.push_back(0);              // size field
        const char ftyp[] = { 'f', 't', 'y', 'p' };
        for (char c : ftyp) buf.push_back(static_cast<uint8_t>(c));
        for (int i = 0; i < 8; i++) buf.push_back(0);              // remainder

        VFS vfs;
        mountTex(vfs, "test_oom_mp4_placeholder", std::move(buf));

        WPTexImageParser parser(&vfs);
        // Must reject — does not allocate ~676 MB and does not crash.
        CHECK(parser.Parse("test_oom_mp4_placeholder") == nullptr);
    }

    TEST_CASE("Decompressed_size at cap is accepted (boundary)") {
        // Pin the cap: 256 MB ≤ decompressed_size must still pass the guard.
        // Use a tiny src so the test stays fast; the LZ4 decode itself will
        // fail and the parser returns nullptr — but for a different reason
        // than the OOM guard.  We can't distinguish those cleanly from a
        // black-box test, so just assert no crash and parser returns cleanly.
        auto buf = makeTexHeader(1, 1, 2, 0, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, 1);                                       // mipmap_count
        appendInt32(buf, 4);                                       // width
        appendInt32(buf, 4);                                       // height
        appendInt32(buf, 1);                                       // LZ4_compressed = true
        appendInt32(buf, 256 * 1024 * 1024);                       // decompressed_size = 256 MB (== cap)
        appendInt32(buf, 16);                                      // src_size = 16
        for (int i = 0; i < 16; i++) buf.push_back(0);

        VFS vfs;
        mountTex(vfs, "test_oom_cap", std::move(buf));

        WPTexImageParser parser(&vfs);
        // Expect nullptr (LZ4_decompress_safe will fail on the 16-byte
        // garbage with decompressed_size at cap), and no crash / no OOM
        // (proved by reaching here).
        CHECK(parser.Parse("test_oom_cap") == nullptr);
    }

    TEST_CASE("Zero src_size returns nullptr") {
        auto buf = makeTexHeader(1, 1, 1, 0, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, 1); // mipmap_count
        appendInt32(buf, 4); // mip width
        appendInt32(buf, 4); // mip height
        appendInt32(buf, 0); // src_size = 0

        VFS vfs;
        mountTex(vfs, "test_zero", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_zero");
        CHECK(img == nullptr);
    }

    TEST_CASE("Zero width returns nullptr (boundary, kills <=0 → <0 mutant)") {
        // width == 0 must hit the early-return.  Under cxx_le_to_lt the guard
        // becomes `< 0` and lets zero through, allocating a 0-size buffer and
        // eventually writing to NULL mipmap.data.
        auto buf = makeTexHeader(1, 1, 1, 0, 0, 0, 4, 0, 4, 1);
        appendInt32(buf, 1);  // mipmap_count
        appendInt32(buf, 0);  // mip width = 0
        appendInt32(buf, 4);  // mip height
        appendInt32(buf, 64); // src_size (non-zero so other checks pass)
        std::vector<uint8_t> pixels(64, 0);
        append(buf, pixels.data(), pixels.size());

        VFS vfs;
        mountTex(vfs, "test_zerow", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_zerow");
        CHECK(img == nullptr);
    }

    TEST_CASE("Zero height returns nullptr (boundary)") {
        auto buf = makeTexHeader(1, 1, 1, 0, 0, 4, 0, 4, 0, 1);
        appendInt32(buf, 1);
        appendInt32(buf, 4);
        appendInt32(buf, 0); // mip height = 0
        appendInt32(buf, 64);
        std::vector<uint8_t> pixels(64, 0);
        append(buf, pixels.data(), pixels.size());

        VFS vfs;
        mountTex(vfs, "test_zeroh", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_zeroh");
        CHECK(img == nullptr);
    }

    TEST_CASE("Negative width returns nullptr") {
        auto buf = makeTexHeader(1, 1, 1, 0, 0, -1, 4, 4, 4, 1);
        appendInt32(buf, 1);
        appendInt32(buf, -1); // mip width < 0
        appendInt32(buf, 4);  // mip height
        appendInt32(buf, 64); // src_size
        std::vector<uint8_t> pixels(64, 0);
        append(buf, pixels.data(), pixels.size());

        VFS vfs;
        mountTex(vfs, "test_negw", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_negw");
        CHECK(img == nullptr);
    }

    TEST_CASE("Negative count returns nullptr") {
        auto buf = makeTexHeader(1, 1, 1, 0, 0, 4, 4, 4, 4, -1);

        VFS vfs;
        mountTex(vfs, "test_negcount", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_negcount");
        CHECK(img == nullptr);
    }

    TEST_CASE("Non-existent file returns nullptr") {
        VFS  vfs;
        auto mockFs = std::make_unique<MockFs>();
        vfs.Mount("/assets", std::move(mockFs));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("nonexistent");
        CHECK(img == nullptr);
    }

    TEST_CASE("Texture flags — clampUVs and noInterpolation") {
        // flags: bit 0 = noInterpolation, bit 1 = clampUVs
        uint32_t flags = (1u << 0) | (1u << 1);
        auto     buf   = makeTexHeader(1, 1, 1, 0, flags, 2, 2, 2, 2, 1);
        appendInt32(buf, 1);
        std::vector<uint8_t> pixels(2 * 2 * 4, 0);
        appendMipmapV1(buf, 2, 2, pixels);

        VFS vfs;
        mountTex(vfs, "test_flags", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_flags");
        REQUIRE(img != nullptr);
        CHECK(img->header.sample.wrapS == TextureWrap::CLAMP_TO_EDGE);
        CHECK(img->header.sample.wrapT == TextureWrap::CLAMP_TO_EDGE);
        CHECK(img->header.sample.magFilter == TextureFilter::NEAREST);
        CHECK(img->header.sample.minFilter == TextureFilter::NEAREST);
    }

    TEST_CASE("Multiple images") {
        auto buf = makeTexHeader(1, 1, 1, 0, 0, 2, 2, 2, 2, 2); // count=2
        for (int i = 0; i < 2; i++) {
            appendInt32(buf, 1); // mipmap_count
            std::vector<uint8_t> pixels(2 * 2 * 4, static_cast<uint8_t>(i + 1));
            appendMipmapV1(buf, 2, 2, pixels);
        }

        VFS vfs;
        mountTex(vfs, "test_multi", std::move(buf));

        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_multi");
        REQUIRE(img != nullptr);
        CHECK(img->header.count == 2);
        REQUIRE(img->slots.size() == 2);
        CHECK(img->slots[0].mipmaps[0].data.get()[0] == 1);
        CHECK(img->slots[1].mipmaps[0].data.get()[0] == 2);
    }

    // --- Mutation testing: TEXB v3/v4 header version branches ---

    // Build texb v3 header (has imageType field after TEXB+count)
    std::vector<uint8_t> makeTexHeaderV3(int32_t imageType, int32_t w, int32_t h) {
        auto buf = makeTexHeader(1, 1, 3, 0, 0, w, h, w, h, 1);
        appendInt32(buf, imageType); // imageType field (texb >= 3)
        return buf;
    }

    // Build texb v4 header (has imageType + isVideoMp4 fields)
    std::vector<uint8_t> makeTexHeaderV4(int32_t imageType, int32_t w, int32_t h) {
        auto buf = makeTexHeader(1, 1, 4, 0, 0, w, h, w, h, 1);
        appendInt32(buf, imageType); // imageType (texb >= 3)
        appendInt32(buf, 0);         // isVideoMp4 (texb >= 4)
        return buf;
    }

    TEST_CASE("TEXB v3 header reads imageType — parses successfully") {
        // texb==3 must read the imageType field; if ge_to_gt mutant fires
        // (>= 3 becomes > 3), the imageType int won't be consumed and
        // the stream will be misaligned, causing a parse failure.
        // Use imageType=UNKNOWN(-1) so the parser takes the raw-copy branch
        // instead of stbi_load — the raw RGBA payload here is not a valid
        // image container, and the stbi pre-validation guard would reject it.
        auto buf = makeTexHeader(1, 1, 3, 0, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, -1); // imageType = UNKNOWN (texb >= 3)
        appendInt32(buf, 1); // mipmap count
        std::vector<uint8_t> pixels(4 * 4 * 4, 0xCC);
        appendMipmapV2(buf, 4, 4, pixels);

        VFS vfs;
        mountTex(vfs, "texb3_test", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("texb3_test");
        REQUIRE(img != nullptr);
        CHECK(img->slots[0].width == 4);
        CHECK(img->slots[0].height == 4);
    }

    TEST_CASE("TEXB v4 header reads imageType and isVideoMp4") {
        // imageType=UNKNOWN(-1) routes through the raw-copy branch (the raw
        // RGBA payload here is not a valid image container, so the stbi
        // pre-validation guard would otherwise reject it).
        auto buf = makeTexHeader(1, 1, 4, 0, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, -1); // imageType = UNKNOWN (texb >= 3)
        appendInt32(buf, 0); // isVideoMp4 flag    (texb >= 4)
        appendInt32(buf, 1); // mipmap count
        appendInt32(buf, 4); // w
        appendInt32(buf, 4); // h
        appendInt32(buf, 0);
        appendInt32(buf, 0); // LZ4
        std::vector<uint8_t> pixels(4 * 4 * 4, 0xDD);
        appendInt32(buf, (int32_t)pixels.size());
        append(buf, pixels.data(), pixels.size());

        VFS vfs;
        mountTex(vfs, "texb4_test", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("texb4_test");
        REQUIRE(img != nullptr);
        CHECK(img->slots[0].width == 4);
        CHECK(img->slots[0].height == 4);
    }

    TEST_CASE("Mipmap size = width * height * sizeof(uint8_t)") {
        // Kills mul_to_div on line 225: mipmap.size = src_size * sizeof(uint8_t)
        // and line 219: src_size = w * h * 4
        auto buf = makeSimpleRGBA8Tex(8, 4);
        VFS  vfs;
        mountTex(vfs, "size_check", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("size_check");
        REQUIRE(img != nullptr);
        CHECK(img->slots[0].mipmaps[0].size == 8 * 4 * 4);
    }

    // --- Mutation testing: sprite with texs=1 (integer frame data) ---

    TEST_CASE("Sprite texs=1 with non-zero coords verifies division") {
        // Kills div_to_mul on lines 299-300: x=(float)ReadInt32()/spriteWidth
        // With x=8, spriteWidth=32: 8/32=0.25, but 8*32=256
        uint32_t spriteFlag = (1u << 2);
        auto     buf        = makeTexHeader(1, 1, 2, 0, spriteFlag, 32, 16, 32, 16, 1);
        appendInt32(buf, 1);
        std::vector<uint8_t> pixels(32 * 16 * 4, 0);
        appendMipmapV2(buf, 32, 16, pixels);
        appendTexVersion(buf, 1); // texs=1 (integer format)
        appendInt32(buf, 1);      // framecount
        appendInt32(buf, 0);      // imageId
        appendFloat(buf, 0.1f);   // frametime
        appendInt32(buf, 8);      // x (int) → 8/32 = 0.25
        appendInt32(buf, 4);      // y (int) → 4/16 = 0.25
        appendInt32(buf, 16);     // xAxis[0]
        appendInt32(buf, 0);      // xAxis[1]
        appendInt32(buf, 0);      // yAxis[0]
        appendInt32(buf, 8);      // yAxis[1]

        VFS vfs;
        mountTex(vfs, "sprite_v1_div", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_v1_div");
        CHECK(header.isSprite == true);
        auto& frame = header.spriteAnim.GetCurFrame();
        CHECK(frame.x == doctest::Approx(0.25f));
        CHECK(frame.y == doctest::Approx(0.25f));
        // xAxis[0]/spriteWidth = 16/32 = 0.5
        CHECK(frame.xAxis[0] == doctest::Approx(0.5f));
        // yAxis[1]/spriteHeight = 8/16 = 0.5
        CHECK(frame.yAxis[1] == doctest::Approx(0.5f));
    }

    TEST_CASE("Sprite texs=1 reads integer frame coordinates") {
        uint32_t spriteFlag = (1u << 2);
        auto     buf        = makeTexHeader(1, 1, 2, 0, spriteFlag, 32, 32, 32, 32, 1);
        // Image 0: 1 mipmap
        appendInt32(buf, 1);
        std::vector<uint8_t> pixels(32 * 32 * 4, 0);
        appendMipmapV2(buf, 32, 32, pixels);
        // Sprite section with texs=1 (integer format)
        appendTexVersion(buf, 1); // texs=1
        appendInt32(buf, 1);      // framecount
        appendInt32(buf, 0);      // imageId
        appendFloat(buf, 0.1f);   // frametime
        // texs==1: 6 int32 values (x, y, xAxis[0], xAxis[1], yAxis[0], yAxis[1])
        appendInt32(buf, 0);  // x (int)
        appendInt32(buf, 0);  // y (int)
        appendInt32(buf, 16); // xAxis[0] (int)
        appendInt32(buf, 0);  // xAxis[1] (int)
        appendInt32(buf, 0);  // yAxis[0] (int)
        appendInt32(buf, 16); // yAxis[1] (int)

        VFS vfs;
        mountTex(vfs, "sprite_v1", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_v1");
        CHECK(header.isSprite == true);
        CHECK(header.spriteAnim.numFrames() == 1);
        auto& frame = header.spriteAnim.GetCurFrame();
        CHECK(frame.imageId == 0);
        CHECK(frame.frametime == doctest::Approx(0.1f));
        // x = 0/32 = 0.0, y = 0/32 = 0.0
        CHECK(frame.x == doctest::Approx(0.0f));
        CHECK(frame.y == doctest::Approx(0.0f));
        // width = sqrt(16^2 + 0^2) = 16
        CHECK(frame.width == doctest::Approx(16.0f));
        // height = sqrt(0^2 + 16^2) = 16
        CHECK(frame.height == doctest::Approx(16.0f));
        // rate = height / width = 1.0
        CHECK(frame.rate == doctest::Approx(1.0f));
    }

    TEST_CASE("Sprite frame coordinate division by sprite dimensions") {
        // Kills div_to_mul: sf.x = (float)file.ReadInt32() / spriteWidth
        uint32_t spriteFlag = (1u << 2);
        auto     buf        = makeTexHeader(1, 1, 2, 0, spriteFlag, 64, 32, 64, 32, 1);
        appendInt32(buf, 1);
        std::vector<uint8_t> pixels(64 * 32 * 4, 0);
        appendMipmapV2(buf, 64, 32, pixels);
        appendTexVersion(buf, 2); // texs=2 (float format)
        appendInt32(buf, 1);      // framecount
        appendInt32(buf, 0);      // imageId
        appendFloat(buf, 0.5f);   // frametime
        appendFloat(buf, 16.0f);  // x (float)
        appendFloat(buf, 8.0f);   // y (float)
        appendFloat(buf, 32.0f);  // xAxis[0]
        appendFloat(buf, 0.0f);   // xAxis[1]
        appendFloat(buf, 0.0f);   // yAxis[0]
        appendFloat(buf, 16.0f);  // yAxis[1]

        VFS vfs;
        mountTex(vfs, "sprite_coords", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_coords");
        CHECK(header.isSprite == true);
        auto& frame = header.spriteAnim.GetCurFrame();
        // x = 16.0/64 = 0.25, y = 8.0/32 = 0.25
        CHECK(frame.x == doctest::Approx(0.25f));
        CHECK(frame.y == doctest::Approx(0.25f));
        // xAxis[0]/spriteWidth = 32/64 = 0.5
        CHECK(frame.xAxis[0] == doctest::Approx(0.5f));
        // yAxis[1]/spriteHeight = 16/32 = 0.5
        CHECK(frame.yAxis[1] == doctest::Approx(0.5f));
        // width = sqrt(32^2 + 0^2) = 32
        CHECK(frame.width == doctest::Approx(32.0f));
        // height = sqrt(0^2 + 16^2) = 16
        CHECK(frame.height == doctest::Approx(16.0f));
        // rate = 16 / 32 = 0.5
        CHECK(frame.rate == doctest::Approx(0.5f));
    }

    TEST_CASE("Sprite texs=3 reads extra width/height fields") {
        // Kills gt_to_ge and gt_to_le on line 275: texs > 3 → error
        // texs=3 should NOT trigger the error; texs=4 should
        uint32_t spriteFlag = (1u << 2);
        auto     buf        = makeTexHeader(1, 1, 2, 0, spriteFlag, 16, 16, 16, 16, 1);
        appendInt32(buf, 1);
        std::vector<uint8_t> pixels(16 * 16 * 4, 0);
        appendMipmapV2(buf, 16, 16, pixels);
        appendTexVersion(buf, 3); // texs=3
        appendInt32(buf, 1);      // framecount
        // texs==3 has extra width/height before frames
        appendInt32(buf, 16); // extra width
        appendInt32(buf, 16); // extra height
        // Frame data (texs >= 2 → float format)
        appendInt32(buf, 0);     // imageId
        appendFloat(buf, 0.5f);  // frametime
        appendFloat(buf, 0.0f);  // x
        appendFloat(buf, 0.0f);  // y
        appendFloat(buf, 16.0f); // xAxis[0]
        appendFloat(buf, 0.0f);  // xAxis[1]
        appendFloat(buf, 0.0f);  // yAxis[0]
        appendFloat(buf, 16.0f); // yAxis[1]

        VFS vfs;
        mountTex(vfs, "sprite_v3", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_v3");
        CHECK(header.isSprite == true);
        CHECK(header.spriteAnim.numFrames() == 1);
    }

    TEST_CASE("Count of zero produces valid empty image") {
        // Kills lt_to_le on line 164: count < 0 returns nullptr
        // count=0 should NOT return nullptr — it's valid (empty slots)
        auto buf = makeTexHeader(1, 1, 1, 0, 0, 4, 4, 4, 4, 0);
        VFS  vfs;
        mountTex(vfs, "test_zero_count", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_zero_count");
        REQUIRE(img != nullptr);
        CHECK(img->slots.size() == 0);
    }

    TEST_CASE("Multiple mipmaps parsed correctly") {
        // Kills post_inc_to_post_dec on loop counter (line 175)
        auto buf = makeTexHeader(1, 1, 1, 0, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, 2); // 2 mipmaps
        // Mipmap 0: 4x4
        std::vector<uint8_t> pix4(4 * 4 * 4, 0xAA);
        appendMipmapV1(buf, 4, 4, pix4);
        // Mipmap 1: 2x2
        std::vector<uint8_t> pix2(2 * 2 * 4, 0xBB);
        appendMipmapV1(buf, 2, 2, pix2);

        VFS vfs;
        mountTex(vfs, "test_2mip", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("test_2mip");
        REQUIRE(img != nullptr);
        REQUIRE(img->slots[0].mipmaps.size() == 2);
        CHECK(img->slots[0].mipmaps[0].width == 4);
        CHECK(img->slots[0].mipmaps[1].width == 2);
    }

    // -----------------------------------------------------------------------
    // stbi container path (TEXB v3+, imageType != UNKNOWN)
    // Kills:
    //   * src_size = w*h*4 mutation (→ w*h*3): size would be 3, not 4
    //   * type != UNKNOWN mutation (→ == UNKNOWN): falls through to raw path, size stays 21
    //   * texb >= 3 is already killed by "TEXB v3 header reads imageType" test
    // -----------------------------------------------------------------------

    // Minimal 1x1 red-pixel TGA (type 2, uncompressed true-color, 24 bpp, BGR order).
    // 18-byte header + 3-byte pixel.  stbi decodes to RGBA {255,0,0,255} when
    // called with n_req=4.
    static const uint8_t kRedPixelTga[] = { /* id_len */ 0x00,
                                            /* cmap_t */ 0x00,
                                            /* img_t  */ 0x02, // uncompressed true-color
                                            /* cmap spec: first_entry, length, entry_size */
                                            0x00,
                                            0x00,
                                            0x00,
                                            0x00,
                                            0x00,
                                            /* x_origin */ 0x00,
                                            0x00,
                                            /* y_origin */ 0x00,
                                            0x00,
                                            /* width   */ 0x01,
                                            0x00, // 1 px (little-endian)
                                            /* height  */ 0x01,
                                            0x00,               // 1 px
                                            /* bpp     */ 0x18, // 24
                                            /* img_desc*/ 0x00,
                                            /* pixel (BGR): blue=0, green=0, red=255 */
                                            0x00,
                                            0x00,
                                            0xFF };

    // Minimal 2x3 TGA (6 pixels, 24bpp). Kills w*h → w/h mutant: 2*3*4=24 vs 2/3*4=0.
    static const uint8_t kTga2x3[] = {
        0x00,
        0x00,
        0x02, // id_len=0, cmap=0, img_type=2
        0x00,
        0x00,
        0x00,
        0x00,
        0x00, // cmap spec
        0x00,
        0x00,
        0x00,
        0x00, // x/y origin
        0x02,
        0x00, // width=2
        0x03,
        0x00, // height=3
        0x18,
        0x00, // bpp=24, img_desc=0
        // 6 pixels (BGR): all red
        0x00,
        0x00,
        0xFF,
        0x00,
        0x00,
        0xFF,
        0x00,
        0x00,
        0xFF,
        0x00,
        0x00,
        0xFF,
        0x00,
        0x00,
        0xFF,
        0x00,
        0x00,
        0xFF,
    };

    TEST_CASE("TEXB v3 stbi path — 2x3 TGA kills w*h mutation") {
        auto buf = makeTexHeader(2, 3, 3, 0, 0, 1, 1, 1, 1, 1);
        appendInt32(buf, 17); // imageType = TARGA
        appendInt32(buf, 1);  // mipmap_count
        std::vector<uint8_t> tgaData(kTga2x3, kTga2x3 + sizeof(kTga2x3));
        appendMipmapV2(buf, 2, 3, tgaData);

        VFS vfs;
        mountTex(vfs, "stbi_2x3", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("stbi_2x3");

        REQUIRE(img != nullptr);
        REQUIRE(img->slots[0].mipmaps.size() == 1);
        auto& mip = img->slots[0].mipmaps[0];
        CHECK(mip.width == 2);
        CHECK(mip.height == 3);
        // w*h*4 = 2*3*4 = 24. If mutated to w/h*4 = 0*4 = 0 (wrong!)
        CHECK(mip.size == 24);
    }

    TEST_CASE("TEXB v3 stbi path — valid TGA decoded to RGBA") {
        // imageType = TARGA (17) → stbi decodes, src_size = w*h*4 = 4
        auto buf = makeTexHeader(1, 1, 3, /*tex_fmt=*/0, /*flags=*/0, 1, 1, 1, 1, /*count=*/1);
        appendInt32(buf, 17); // imageType = TARGA
        appendInt32(buf, 1);  // mipmap_count
        std::vector<uint8_t> tgaData(kRedPixelTga, kRedPixelTga + sizeof(kRedPixelTga));
        appendMipmapV2(buf, 1, 1, tgaData);

        VFS vfs;
        mountTex(vfs, "stbi_tga_v3", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("stbi_tga_v3");

        REQUIRE(img != nullptr);
        REQUIRE(img->slots.size() == 1);
        REQUIRE(img->slots[0].mipmaps.size() == 1);
        auto& mip = img->slots[0].mipmaps[0];
        CHECK(mip.width == 1);
        CHECK(mip.height == 1);
        // src_size is set to w*h*4 after stbi decode — mutation w*h*3 would give 3
        CHECK(mip.size == 4);
        // stbi fills alpha=255 for opaque RGB sources; pixel order is RGBA
        REQUIRE(mip.data.get() != nullptr);
        CHECK(mip.data.get()[0] == 255); // R
        CHECK(mip.data.get()[1] == 0);   // G
        CHECK(mip.data.get()[2] == 0);   // B
        CHECK(mip.data.get()[3] == 255); // A (stbi adds opaque alpha for 24-bit source)
    }

    TEST_CASE("TEXB v3 stbi path skipped when imageType is UNKNOWN") {
        // imageType = UNKNOWN (-1) → raw-copy path, size stays at sizeof(TGA)=21
        // Kills the != UNKNOWN → == UNKNOWN mutant on the branch condition.
        auto buf = makeTexHeader(1, 1, 3, /*tex_fmt=*/0, /*flags=*/0, 1, 1, 1, 1, /*count=*/1);
        appendInt32(buf, -1); // imageType = UNKNOWN
        appendInt32(buf, 1);
        std::vector<uint8_t> tgaData(kRedPixelTga, kRedPixelTga + sizeof(kRedPixelTga));
        appendMipmapV2(buf, 1, 1, tgaData);

        VFS vfs;
        mountTex(vfs, "stbi_unknown_v3", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("stbi_unknown_v3");

        REQUIRE(img != nullptr);
        REQUIRE(img->slots[0].mipmaps.size() == 1);
        auto& mip = img->slots[0].mipmaps[0];
        // Raw path: size = sizeof(TGA) = 21, not 4
        CHECK(mip.size == (int32_t)sizeof(kRedPixelTga));
    }

    TEST_CASE("TEXB v2 stbi path never triggered — raw copy always used") {
        // texb=2: condition texb>=3 is false even though imageType field is absent.
        // Raw data kept as-is; size = sizeof(TGA) = 21.
        auto buf = makeTexHeader(1, 1, 2, /*tex_fmt=*/0, /*flags=*/0, 1, 1, 1, 1, /*count=*/1);
        // No imageType field for texb=2
        appendInt32(buf, 1);
        std::vector<uint8_t> tgaData(kRedPixelTga, kRedPixelTga + sizeof(kRedPixelTga));
        appendMipmapV2(buf, 1, 1, tgaData);

        VFS vfs;
        mountTex(vfs, "stbi_v2_raw", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("stbi_v2_raw");

        REQUIRE(img != nullptr);
        REQUIRE(img->slots[0].mipmaps.size() == 1);
        auto& mip = img->slots[0].mipmaps[0];
        // Raw path: size = 21
        CHECK(mip.size == (int32_t)sizeof(kRedPixelTga));
    }

    // -----------------------------------------------------------------------
    // Long-tail mutation killers
    // -----------------------------------------------------------------------

    // src_size==8 with non-MP4 payload exposes the `src_size > 8` boundary.
    // Original lets size==8 fall through to memcmp (which fails, so we hit the
    // raw-copy path).  Under cxx_gt_to_ge: still enters memcmp branch (matches
    // original).  Under cxx_gt_to_le: NEVER enters MP4 branch — also still
    // raw-copy path.  Together with the second clause `result.size() >= 8`,
    // they require an 8-byte non-"ftyp" payload to discriminate.  Use a buffer
    // that DOES have "ftyp" at offset 4 — under cxx_gt_to_ge or cxx_gt_to_le
    // the MP4 detection at size==8 must fire (or not) consistently, and the
    // behavior must match.
    TEST_CASE("MP4 detection requires src_size > 8 (boundary at 8 bytes)") {
        // src_size = 8 exactly with "ftyp" at offset 4 — boundary: original
        // `> 8` is false → raw-copy path → size = 8.  Under `>= 8` the MP4
        // branch fires, sets isVideoTexture=true, and overwrites size with
        // Rgba8ByteSize(w, h) = 4*4*4 = 64.
        auto buf = makeTexHeader(1, 1, 2, 0, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, 1);                                          // mipmap_count
        appendInt32(buf, 4);                                          // mip width
        appendInt32(buf, 4);                                          // mip height
        appendInt32(buf, 0);                                          // LZ4_compressed = false
        appendInt32(buf, 0);                                          // decompressed_size
        appendInt32(buf, 8);                                          // src_size = 8
        // 8 raw bytes: 4 bytes pad + "ftyp"
        const uint8_t fake[8] = { 0x00, 0x00, 0x00, 0x20, 'f', 't', 'y', 'p' };
        for (uint8_t b : fake) buf.push_back(b);

        VFS vfs;
        mountTex(vfs, "mp4_boundary", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("mp4_boundary");
        REQUIRE(img != nullptr);
        // MP4 branch must NOT fire at size==8 (original `> 8` is false).
        CHECK_FALSE(img->header.isVideoTexture);
        REQUIRE(img->slots[0].mipmaps.size() == 1);
        // Raw-copy path keeps src_size=8.
        CHECK(img->slots[0].mipmaps[0].size == 8);
    }

    // src_size > 8 with "ftyp" at offset 4 must trigger the MP4 path.  Pairs
    // with the above to pin down the boundary in both directions.  Without
    // this, cxx_gt_to_le (which makes the guard `<= 8`) wouldn't be detected.
    TEST_CASE("MP4 detection fires at src_size > 8 with ftyp signature") {
        auto buf = makeTexHeader(1, 1, 2, 0, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, 1);
        appendInt32(buf, 4);
        appendInt32(buf, 4);
        appendInt32(buf, 0);  // LZ4 false
        appendInt32(buf, 0);  // decompressed_size
        appendInt32(buf, 12); // src_size = 12 (>8)
        // 12 bytes: 4 bytes pad + "ftyp" + 4 trailing bytes
        const uint8_t fake[12] = {
            0x00, 0x00, 0x00, 0x20, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'
        };
        for (uint8_t b : fake) buf.push_back(b);

        VFS vfs;
        mountTex(vfs, "mp4_detect", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("mp4_detect");
        REQUIRE(img != nullptr);
        // MP4 branch fires → isVideoTexture true, size set to RGBA placeholder.
        CHECK(img->header.isVideoTexture);
        REQUIRE(img->slots[0].mipmaps.size() == 1);
        // RGBA placeholder of 4x4 = 64 bytes.
        CHECK(img->slots[0].mipmaps[0].size == 64);
    }

    // ParseHeader with count==0 in the non-sprite path must read the trailing
    // mipmap_count + width + height and call SetHeaderPow2 — kills the
    // cxx_lt_to_le mutation on `header.count < 0` (which would convert it to
    // `<= 0` and early-return at count==0, leaving mipmap_pow2 unset).
    TEST_CASE("ParseHeader with count==0 still reads trailing mipmap_pow2 size") {
        auto buf = makeTexHeader(1, 1, 1, /*type*/ 0, /*flags*/ 0,
                                 /*tex_w*/ 4, /*tex_h*/ 4,
                                 /*map_w*/ 4, /*map_h*/ 4,
                                 /*count*/ 0);
        // Non-sprite trailing: mipmap_count, width, height
        appendInt32(buf, 1);
        appendInt32(buf, 8); // width = 8 (pow2)
        appendInt32(buf, 8); // height = 8

        VFS vfs;
        mountTex(vfs, "header_count_zero", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("header_count_zero");
        // SetHeaderPow2 sets mipmap_pow2 from one of (8, 8) → both pow2 → true.
        CHECK(header.mipmap_pow2 == true);
        CHECK(header.count == 0);
    }

    // Multi-image .tex: parser must iterate i_image until image_count.  Under
    // cxx_post_inc_to_post_dec on the i_image loop, only image[0] is written
    // and slots[1] stays default-constructed (width/height=0).
    TEST_CASE("Multiple images with distinct sizes verifies image-loop ++ (kills i++ → i--)") {
        // count=2: two images with distinct dimensions.
        auto buf = makeTexHeader(1, 1, 1, 0, 0, 4, 4, 4, 4, 2);
        // image 0: 4x4
        appendInt32(buf, 1);
        std::vector<uint8_t> p0(4 * 4 * 4, 0xAA);
        appendMipmapV1(buf, 4, 4, p0);
        // image 1: 8x2 (distinct dimensions from image 0)
        appendInt32(buf, 1);
        std::vector<uint8_t> p1(8 * 2 * 4, 0xBB);
        appendMipmapV1(buf, 8, 2, p1);

        VFS vfs;
        mountTex(vfs, "multi_distinct", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("multi_distinct");
        REQUIRE(img != nullptr);
        REQUIRE(img->slots.size() == 2);
        // image[1] must be parsed — under i-- mutation it'd remain default.
        CHECK(img->slots[1].width == 8);
        CHECK(img->slots[1].height == 2);
        REQUIRE(img->slots[1].mipmaps.size() == 1);
        CHECK(img->slots[1].mipmaps[0].width == 8);
        CHECK(img->slots[1].mipmaps[0].height == 2);
        CHECK(img->slots[1].mipmaps[0].data.get()[0] == 0xBB);
    }

    // Sprite header parser branches on `texb > 1` to consume two extra
    // int32 fields per mipmap (LZ4_compressed + decompressed_size).  Under
    // cxx_gt_to_ge (`>= 1`), texb=1 sprites also try to consume those — the
    // stream then misaligns and the sprite section fails to parse.  Test pins
    // texb=1 sprite parsing.  Note: texb v1 sprites are rare but the parse
    // must remain stable.
    TEST_CASE("Sprite with texb=1 parses without consuming LZ4 fields (kills texb val>1 → val>=1)") {
        // Build a sprite with texb=1 (NO LZ4 fields between width/height and
        // src_size).  The sprite section that follows the mipmap data must be
        // parseable; under the mutation, the parser would consume 8 extra
        // bytes per mipmap and corrupt the stream.
        uint32_t spriteFlag = (1u << 2);
        auto     buf        = makeTexHeader(1, 1, 1, 0, spriteFlag, 16, 16, 16, 16, 1);
        // Image 0: 1 mipmap, texb=1 layout (no LZ4 fields)
        appendInt32(buf, 1); // mipmap_count
        std::vector<uint8_t> pixels(16 * 16 * 4, 0xCD);
        appendMipmapV1(buf, 16, 16, pixels); // texb=1: w, h, size, pixels — no LZ4 fields

        // Sprite section
        appendTexVersion(buf, 2); // texs version
        appendInt32(buf, 1);      // framecount
        appendInt32(buf, 0);      // imageId = 0 (valid)
        appendFloat(buf, 0.5f);   // frametime
        appendFloat(buf, 0.0f);   // x
        appendFloat(buf, 0.0f);   // y
        appendFloat(buf, 16.0f);  // xAxis[0]
        appendFloat(buf, 0.0f);   // xAxis[1]
        appendFloat(buf, 0.0f);   // yAxis[0]
        appendFloat(buf, 16.0f);  // yAxis[1]

        VFS vfs;
        mountTex(vfs, "sprite_texb1", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             header = parser.ParseHeader("sprite_texb1");
        // Sprite must parse cleanly.  Under the mutation the framecount /
        // imageId reads garbage and the sprite is disabled.
        CHECK(header.isSprite == true);
        CHECK(header.spriteAnim.numFrames() == 1);
        auto& frame = header.spriteAnim.GetCurFrame();
        CHECK(frame.imageId == 0);
        CHECK(frame.frametime == doctest::Approx(0.5f));
    }

    TEST_CASE("Hostile image_count does not OOM-resize") {
        // Fuzz-found regression. img.header.count came straight from the .tex
        // header into img.slots.resize(image_count) with no upper bound — a
        // count near INT32_MAX requested ~80 GB. CountFitsStream now bounds
        // it against bytes-remaining; Parse should return nullptr cleanly.
        auto buf = makeTexHeader(1, 1, 1, /*type=RGBA8*/ 0, /*flags=*/0, 4, 4, 4, 4,
                                 /*count=*/0x10000000);
        VFS vfs;
        mountTex(vfs, "hostile_count", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("hostile_count");
        CHECK(img == nullptr);
    }

    // -----------------------------------------------------------------------
    // Fuzz-hardening bounds: image_count, mipmap_count, mip dims, cumulative
    // byte budget, and stbi pre-validation.  Each test pins one rejection
    // branch in Parse().  Real WP textures sit well under all caps (≤6 slots,
    // ≤13 mips, ≤4K per side); hostile inputs trip the cap and return nullptr
    // without ever allocating the budget they declared.
    // -----------------------------------------------------------------------

    TEST_CASE("Plausibility cap on image_count rejects large stream-fitting values") {
        // 32 slots × 1 byte/slot = 32 bytes — fits the stream, so the
        // pre-existing CountFitsStream check accepts it.  The new plausibility
        // cap (16 slots) is what rejects this — WP ships at most 6 (cube map).
        auto buf = makeTexHeader(1, 1, 1, 0, 0, 4, 4, 4, 4, /*count=*/32);
        // Pad with bytes so CountFitsStream(file, 32) succeeds.
        for (int i = 0; i < 256; i++) buf.push_back(0);
        VFS vfs;
        mountTex(vfs, "many_slots", std::move(buf));
        WPTexImageParser parser(&vfs);
        CHECK(parser.Parse("many_slots") == nullptr);
    }

    TEST_CASE("Plausibility cap on mipmap_count rejects large stream-fitting values") {
        // 64 mips × 1 byte = 64 bytes — fits stream.  Plausibility cap (24)
        // rejects — WP ships at most 13 mips (log2(8K)).
        auto buf = makeTexHeader(1, 1, 1, 0, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, /*mipmap_count=*/64);
        for (int i = 0; i < 512; i++) buf.push_back(0);
        VFS vfs;
        mountTex(vfs, "many_mips", std::move(buf));
        WPTexImageParser parser(&vfs);
        CHECK(parser.Parse("many_mips") == nullptr);
    }

    TEST_CASE("Mip dimension cap rejects oversized width/height") {
        // mipmap.width = 32768 exceeds the 16384 plausibility cap.
        auto buf = makeTexHeader(1, 1, 1, 0, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, 1);                                    // mipmap_count
        appendInt32(buf, /*mip width=*/32768);
        appendInt32(buf, /*mip height=*/4);
        appendInt32(buf, /*src_size=*/16);
        for (int i = 0; i < 16; i++) buf.push_back(0);
        VFS vfs;
        mountTex(vfs, "huge_w", std::move(buf));
        WPTexImageParser parser(&vfs);
        CHECK(parser.Parse("huge_w") == nullptr);
    }

    TEST_CASE("Mip dimension cap accepts 16384 (boundary)") {
        // 16384 × 1 is right at the cap and must NOT be rejected by the
        // dimension guard — the cap is `>`, not `>=`.  Parse succeeds via the
        // raw-copy path; the only assertion is no-crash + no-OOM (proved by
        // reaching here).
        auto buf = makeTexHeader(1, 1, 1, 0, 0, 16384, 1, 16384, 1, 1);
        appendInt32(buf, 1);
        appendInt32(buf, 16384);
        appendInt32(buf, 1);
        appendInt32(buf, 4);
        for (int i = 0; i < 4; i++) buf.push_back(0);
        VFS vfs;
        mountTex(vfs, "edge_dim", std::move(buf));
        WPTexImageParser parser(&vfs);
        auto             img = parser.Parse("edge_dim");
        REQUIRE(img != nullptr);
        CHECK(img->slots[0].width == 16384);
        CHECK(img->slots[0].height == 1);
    }

    TEST_CASE("Cumulative-byte cap rejects N mips × per-mip cap") {
        // 8 mips × 200 MB lz4 decompressed_size each = 1.6 GB cumulative — the
        // per-allocation cap (256 MB) lets each mip past on its own, but the
        // total exceeds the 1 GiB cumulative cap.  Verifies the cumulative
        // budget gate.
        auto buf = makeTexHeader(1, 1, 2, 0, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, /*mipmap_count=*/8);
        for (int i = 0; i < 8; i++) {
            appendInt32(buf, 4);                              // mip width
            appendInt32(buf, 4);                              // mip height
            appendInt32(buf, 1);                              // LZ4 compressed
            appendInt32(buf, 200 * 1024 * 1024);              // decompressed_size = 200MB
            appendInt32(buf, 16);                             // src_size
            for (int j = 0; j < 16; j++) buf.push_back(0);
        }
        VFS vfs;
        mountTex(vfs, "cumulative", std::move(buf));
        WPTexImageParser parser(&vfs);
        CHECK(parser.Parse("cumulative") == nullptr);
    }

    TEST_CASE("stbi pre-validation rejects embedded TGA with huge declared dims") {
        // 10000×10000 TGA inside an otherwise-empty .tex: stbi would allocate
        // ~400MB output before failing on truncated input.  The pre-validation
        // guard rejects up front by reading stbi_info_from_memory.
        auto buf = makeTexHeader(1, 1, 3, 0, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, /*imageType=*/17);  // TARGA
        appendInt32(buf, /*mipmap_count=*/1);
        // TGA header: 18 bytes, declares 10000x10000, 24bpp uncompressed.
        std::vector<uint8_t> tga(18, 0);
        tga[2]  = 2;  // image type: uncompressed true-color
        tga[12] = 0x10; tga[13] = 0x27;  // width = 10000 (little-endian)
        tga[14] = 0x10; tga[15] = 0x27;  // height = 10000
        tga[16] = 24;  // bpp
        appendMipmapV2(buf, /*mip_w=*/4, /*mip_h=*/4, tga);
        VFS vfs;
        mountTex(vfs, "stbi_huge_dim", std::move(buf));
        WPTexImageParser parser(&vfs);
        CHECK(parser.Parse("stbi_huge_dim") == nullptr);
    }

    TEST_CASE("stbi pre-validation rejects embedded TGA exceeding mip container dims") {
        // 100×100 TGA in a 4×4 mip container — the parser overwrites
        // mipmap.size with stbi's w*h*4, so silently accepting the larger
        // embedded image would propagate a mismatched-dim mip downstream.
        // Reject at the parser instead.
        auto buf = makeTexHeader(1, 1, 3, 0, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, /*imageType=*/17);  // TARGA
        appendInt32(buf, /*mipmap_count=*/1);
        std::vector<uint8_t> tga(18, 0);
        tga[2]  = 2;
        tga[12] = 100; tga[13] = 0;
        tga[14] = 100; tga[15] = 0;
        tga[16] = 24;
        appendMipmapV2(buf, /*mip_w=*/4, /*mip_h=*/4, tga);
        VFS vfs;
        mountTex(vfs, "stbi_mip_mismatch", std::move(buf));
        WPTexImageParser parser(&vfs);
        CHECK(parser.Parse("stbi_mip_mismatch") == nullptr);
    }

    TEST_CASE("stbi pre-validation rejects malformed embedded image (info-fails)") {
        // Bytes that don't decode as any image format stbi understands: the
        // pre-validation gate rejects rather than letting stbi do partial
        // allocation work before failing.
        auto buf = makeTexHeader(1, 1, 3, 0, 0, 4, 4, 4, 4, 1);
        appendInt32(buf, /*imageType=*/17);  // TARGA
        appendInt32(buf, /*mipmap_count=*/1);
        std::vector<uint8_t> garbage(64, 0xAB);  // not a valid image header
        appendMipmapV2(buf, /*mip_w=*/4, /*mip_h=*/4, garbage);
        VFS vfs;
        mountTex(vfs, "stbi_garbage", std::move(buf));
        WPTexImageParser parser(&vfs);
        CHECK(parser.Parse("stbi_garbage") == nullptr);
    }

} // TEST_SUITE

// ===========================================================================
// ParseHeader header cache — second call for the same name must not re-open
// the file.  Open-count assertions are the ground truth; value-equality
// assertions prove the cached return is bit-identical to what a fresh parse
// would produce.
// ===========================================================================

TEST_SUITE("WPTexImageParser.ParseHeaderCache") {

    TEST_CASE("Second ParseHeader call does not re-open the file") {
        // Build the VFS manually so we keep a raw pointer to the MockFs for
        // open-count queries after ownership has been transferred to the VFS.
        VFS   vfs;
        auto  fsOwned = std::make_unique<MockFs>();
        auto* mock    = fsOwned.get();
        mock->AddFile("/materials/cache_rgba8.tex", makeSimpleRGBA8Tex(8, 8));
        vfs.Mount("/assets", std::move(fsOwned));

        WPTexImageParser parser(&vfs);

        auto h1 = parser.ParseHeader("cache_rgba8");
        auto h2 = parser.ParseHeader("cache_rgba8");

        // The file is at "/materials/cache_rgba8.tex" inside the MockFs.
        // After the cache lands the second ParseHeader must NOT call Open again.
        CHECK(mock->openCount("/materials/cache_rgba8.tex") == 1);

        // Both calls return bit-identical headers.
        CHECK(h1.width     == h2.width);
        CHECK(h1.height    == h2.height);
        CHECK(h1.mapWidth  == h2.mapWidth);
        CHECK(h1.mapHeight == h2.mapHeight);
        CHECK(h1.isSprite  == h2.isSprite);
        CHECK(h1.format    == h2.format);
        CHECK(h1.count     == h2.count);
    }

    TEST_CASE("m_registered fast-path serves ParseHeader without any file open") {
        // RegisterImage pre-populates m_registered; ParseHeader's very first
        // check hits it — neither the header cache nor the VFS is consulted.
        VFS   vfs;
        auto  fsOwned = std::make_unique<MockFs>();
        auto* mock    = fsOwned.get();
        vfs.Mount("/assets", std::move(fsOwned));

        WPTexImageParser parser(&vfs);

        auto img           = std::make_shared<Image>();
        img->key           = "prebuilt";
        img->header.width  = 32;
        img->header.height = 16;
        img->header.format = TextureFormat::RGBA8;
        img->header.count  = 1;
        parser.RegisterImage("prebuilt", img);

        auto h = parser.ParseHeader("prebuilt");
        CHECK(mock->openCount("/materials/prebuilt.tex") == 0);
        CHECK(h.width  == 32);
        CHECK(h.height == 16);
    }

    TEST_CASE("Missing file is not cached — headerCacheSize stays zero") {
        // A name whose file does not exist triggers the nullptr-open early-return
        // path in ParseHeader.  That sentinel header must NOT be written into
        // m_headerCache — so repeated calls must not poison the cache.
        VFS   vfs;
        auto  fsOwned = std::make_unique<MockFs>();
        auto* mock    = fsOwned.get();
        // No file added — Contains() returns false, so VFS short-circuits
        // before even calling MockFs::Open on the missing path.
        vfs.Mount("/assets", std::move(fsOwned));
        (void)mock; // retained for potential future counter extensions

        WPTexImageParser parser(&vfs);

        // "missing" is not an alias name, so the alias-fallback guard
        // (IsAliasTexture) won't intercept — the VFS miss falls through to the
        // nullptr-open early-return path (before the write-back).
        auto h1 = parser.ParseHeader("missing");
        auto h2 = parser.ParseHeader("missing");

        // headerCacheSize() must be zero — the failed-open path is not cached.
        CHECK(parser.headerCacheSize() == 0);
        // Both returned the same default-initialized header.
        CHECK(h1.width  == h2.width);
        CHECK(h1.height == h2.height);
    }

    TEST_CASE("Sprite header cached correctly — frame data identical on second call") {
        // Sprite textures have per-frame scan loops in ParseHeader.  Caching
        // must preserve the full SpriteAnimation, not just scalar fields.
        uint32_t             spriteFlag = (1u << 2);
        std::vector<uint8_t> buf =
            makeTexHeader(1, 1, 2, 0, spriteFlag, 16, 16, 16, 16, 1);
        appendInt32(buf, 1);                          // mipmap_count
        std::vector<uint8_t> pixels(16 * 16 * 4, 0);
        appendMipmapV2(buf, 16, 16, pixels);
        appendTexVersion(buf, 2); // texs
        appendInt32(buf, 2);      // framecount = 2
        for (int i = 0; i < 2; i++) {
            appendInt32(buf, 0);     // imageId = 0
            appendFloat(buf, 0.1f);  // frametime
            appendFloat(buf, 0.0f);  // x
            appendFloat(buf, 0.0f);  // y
            appendFloat(buf, 16.0f); // xAxis[0]
            appendFloat(buf, 0.0f);  // xAxis[1]
            appendFloat(buf, 0.0f);  // yAxis[0]
            appendFloat(buf, 16.0f); // yAxis[1]
        }

        VFS   vfs;
        auto  fsOwned = std::make_unique<MockFs>();
        auto* mock    = fsOwned.get();
        mock->AddFile("/materials/sprite_cached.tex", std::move(buf));
        vfs.Mount("/assets", std::move(fsOwned));

        WPTexImageParser parser(&vfs);

        auto h1 = parser.ParseHeader("sprite_cached");
        auto h2 = parser.ParseHeader("sprite_cached");

        CHECK(mock->openCount("/materials/sprite_cached.tex") == 1);
        CHECK(h1.isSprite == true);
        CHECK(h2.isSprite == true);
        CHECK(h1.spriteAnim.numFrames() == h2.spriteAnim.numFrames());
        // Frame 0 identical across both calls.
        const auto& f1 = h1.spriteAnim.GetCurFrame();
        const auto& f2 = h2.spriteAnim.GetCurFrame();
        CHECK(f1.imageId == f2.imageId);
        CHECK(f1.frametime == doctest::Approx(f2.frametime));
    }

} // WPTexImageParser.ParseHeaderCache

// ---------------------------------------------------------------------------
// Fuzz crash regression replay.
//
// Iterates tests/fixtures/fuzz_regressions/WPTexImageParser/*.bin and feeds
// each file through the same entry point fuzz_WPTexImageParser drives.
// ---------------------------------------------------------------------------

#include "test_data_root.hpp"

#include <filesystem>
#include <fstream>
#include <iterator>

TEST_SUITE("regression: minimised fuzz crashes") {
    TEST_CASE("regression: minimised fuzz crashes round-trip cleanly") {
        namespace fs2 = std::filesystem;
        const fs2::path dir = wallpaper::test::test_data_root()
                              / "fuzz_regressions" / "WPTexImageParser";
        if (! fs2::exists(dir)) return;
        for (auto& entry : fs2::directory_iterator(dir)) {
            if (entry.path().extension() != ".bin") continue;
            SUBCASE(entry.path().filename().string().c_str()) {
                std::ifstream in(entry.path(), std::ios::binary);
                std::vector<uint8_t> buf(std::istreambuf_iterator<char>(in), {});
                VFS  vfs;
                auto mockFs = std::make_unique<MockFs>();
                mockFs->AddFile("/materials/fuzz.tex", std::move(buf));
                vfs.Mount("/assets", std::move(mockFs));
                WPTexImageParser parser(&vfs);
                CHECK_NOTHROW((void)parser.Parse("fuzz"));
            }
        }
    }
}
